import { extend } from '../shared/index'
import type { key, target, fn } from './index.d'
/**一个装着该键值对应的全部依赖函数的Set */
type Dep = Set<ReactiveEffect>
/**该响应式对象对应的全部键值Map （需要响应式的键值才会被收集进来） */
type DepsMap = Map<key, Dep>
/**runner函数 */
export type runner = fn & {
    /**这个runner函数对应的ReactiveEffect实例 */
    effect?: ReactiveEffect
}
/**调用effect时，第二个参数的可选配置 */
interface options {
    /**指定任务调度器。如果传递了这个，第一次effect初始化的时候会执行fn，后续updated的时候只执行scheduler  */
    scheduler?: fn
    /**指定调用stop后的回调函数 */
    onStop?: fn
}

/**装有所有reactive的Map，可以索引到某个对象的键Map */
const targetMap = new WeakMap<target, DepsMap>()
/**当前正在使用的Effect，在effect被run的时候会设置。因为当我们get某个响应式对象的属性时，会使用track收集依赖，需要知道是在收集哪个函数依赖，所以用一个全局变量装着 */
let activeEffect: ReactiveEffect | undefined;
/**当前是否应该在track函数中收集依赖。注意每次打开后都要关闭。 因为当调用stop后，如果get了响应式对象，仍然会进行收集依赖，这和我们stop方法相违背了 */
let shouldTrack: boolean = false

/**判断是否应该收集依赖 */
export function isTracking(): boolean {
    return shouldTrack && (activeEffect !== undefined)
}

/**reactive的effect类 */
export class ReactiveEffect {
    /**这个effect的fn */
    fn: fn
    /**任务调度器。 如果传递了这个，第一次effect初始化的时候会执行fn，后续updated的时候只执行scheduler */
    scheduler?: fn
    /**这个ReactiveEffect身上被绑定过的dep */
    deps: Dep[] = []
    /**一个flag，用于判断当前effect是否活跃  （如果调用了stop，就停止effect了，就不活跃了） 可以优化性能 */
    active: boolean = true
    /**调用stop后的回调函数 */
    onStop?: fn

    /** 构造函数
     * @param fn effect主函数
     * @param scheduler 任务调度器。如果传递了这个，第一次effect初始化的时候会执行fn，后续updated的时候只执行scheduler 
     */
    constructor(fn: fn, scheduler?: fn) {
        this.fn = fn
        if (scheduler) this.scheduler = scheduler
    }

    /**执行这个依赖函数 */
    run() {
        if (!this.active) { //如果是被stop了的
            return this.fn() //因为 effect 函数需要返回fn，且执行返回值，可以得到fn的返回值，所以这里需要把返回值return出去
        } else {
            shouldTrack = true //如果没被stop，就打开这个开关
            activeEffect = this
            const res = this.fn()
            shouldTrack = false //用完记得关闭
            return res
        }
    }
    /**停止响应式 */
    stop() {
        if (this.active) {//优化性能
            cleanupEffect(this)
            this.active = false
            if (this.onStop) this.onStop()//触发回调函数 
        }
    }
}
/** effect函数
 * @param fn 要执行的依赖函数
 * @param options 配置项
 * @returns 返回runner函数，即这个依赖函数对应的 ReactiveEffect.run 函数
 */
export function effect(fn: fn, options?: options): runner {
    const _effect = new ReactiveEffect(fn, options?.scheduler)
    extend(_effect, options) // options 上可能有很多值，用这个方法可以直接把option的值挂载到_effect，不用写那么多句代码
    _effect.run()
    /**返回出去的runner函数 */
    const runner = _effect.run.bind(_effect) as runner//因为在run函数中，涉及到this的问题，所以这里返回出去时，需要绑定一下this
    runner.effect = _effect //在这里给runner挂载一下effect实例，方便我们后面stop
    return runner
}
/** 依赖收集函数，给 target.key 收集依赖
 * @param target 目标对象
 * @param key 目标键值
 */
export function track(target: target, key: key) {
    if (!isTracking()) return
    //基于target和key，找到对应的Set，往里面添加依赖函数
    /**拿到target对应的Map，装着所有键值Map。*/
    let depsMap = targetMap.get(target)
    if (!depsMap) {//没有的话就初始化，并且放到全局的响应式Map中
        depsMap = new Map()
        targetMap.set(target, depsMap)
    }
    /**拿到装着该键值依赖的容器Set。*/
    let dep = depsMap.get(key)
    if (!dep) {//没有的话就初始化，并且放到对应Map中
        dep = new Set()
        depsMap.set(key, dep)
    }
    trackEffects(dep)
}
/**把当前活跃的依赖activeEffect添加到目标dep中。从track抽离出来是为了让ref复用
 * @param dep 要被添加的dep 
 */
export function trackEffects(dep: Dep) {
    if (dep.has(activeEffect!)) return //如果已经有了，就不需要添加了
    dep.add(activeEffect!) // 收集
    activeEffect!.deps.push(dep)
}



/** 触发指定依赖
 * @param target 目标对象
 * @param key 目标键值
 */
export function trigger(target: target, key: key) {
    //基于target和key，找到对应的Set，把依赖全部执行
    const depsMap = targetMap.get(target)
    if (!depsMap) return
    const dep = depsMap.get(key)
    if (!dep) return
    triggerEffects(dep)
}
/**触发dep中的所有依赖。从trigger抽离出来是为了让ref复用 
 * @param dep 目标dep
 */
export function triggerEffects(dep: Dep) {
    for (const eff of dep) {
        if (eff.scheduler) eff.scheduler() //有 scheduler 就执行 scheduler
        else eff.run()//没有的话就执行普通的run
    }
}
/** 停止响应式
 * @param runner runner函数，将会调用runner身上对应的实例的stop方法
 */
export function stop(runner: runner) {
    runner.effect!.stop()
}
/**清空这个effect在dep中对应的内容 
 * @param effect 要清空的effect
 */
export function cleanupEffect(effect: ReactiveEffect) {
    effect.deps.forEach(dep => { //在这个dep身上把自己删掉
        dep.delete(effect)
    });
}